require 'rex/text/table'

module Msf
  class Plugin::Alias < Msf::Plugin
    class AliasCommandDispatcher
      include Msf::Ui::Console::CommandDispatcher

      attr_reader :aliases

      def initialize(driver)
        super(driver)
        @aliases = {}
      end

      def name
        'Alias'
      end

      @@alias_opts = Rex::Parser::Arguments.new(
        '-h' => [ false, 'Help banner.' ],
        '-c' => [ true, 'Clear an alias (* to clear all).'],
        '-f' => [ true, 'Force an alias assignment.' ]
      )
      #
      # Returns the hash of commands supported by this dispatcher.
      #
      # driver.dispatcher_stack[3].commands
      def commands
        {
          'alias' => 'create or view an alias.'
          #			"alias_clear" => "clear an alias (or all aliases).",
          #			"alias_force" => "Force an alias (such as to override)"
        }.merge(aliases) # make aliased commands available as commands of their own
      end

      #
      # the main alias command handler
      #
      # usage: alias [options] [name [value]]
      def cmd_alias(*args)
        # we parse args manually instead of using @@alias.opts.parse to handle special cases
        case args.length
        when 0 # print the list of current aliases
          if @aliases.empty?
            return print_status('No aliases currently defined')
          else
            tbl = Rex::Text::Table.new(
              'Header' => 'Current Aliases',
              'Prefix' => "\n",
              'Postfix' => "\n",
              'Columns' => [ '', 'Alias Name', 'Alias Value' ]
            )
            # add 'alias' in front of each row so that the output can be copy pasted into an rc file if desired
            @aliases.each_pair do |key, val|
              tbl << ['alias', key, val]
            end
            return print(tbl.to_s)
          end
        when 1 # display the alias if one matches this name (or help)
          return cmd_alias_help if (args[0] == '-h') || (args[0] == '--help')

          if @aliases.keys.include?(args[0])
            print_status("\'#{args[0]}\' is aliased to \'#{@aliases[args[0]]}\'")
          else
            print_status("\'#{args[0]}\' is not currently aliased")
          end
        else # let's see if we can assign or clear the alias
          force = false
          clear = false
          # if using -f or -c, they must be the first arg, because -f/-c may also show up in the alias
          # value so we can't do something like if args.include("-f") or delete_if etc
          # we should never have to force and clear simultaneously.
          if args[0] == '-f'
            force = true
            args.shift
          elsif args[0] == '-c'
            clear = true
            args.shift
          end
          name = args.shift
          # alias name can NEVER be certain reserved words like 'alias', add any other reserved words here
          # We prevent the user from naming the alias "alias" cuz they could end up unable to clear the aliases,
          # for example you 'alias -f set unset and then 'alias -f alias sessions', now you're screwed.  The byproduct
          # of this is that it prevents you from aliasing 'alias' to 'alias -f' etc, but that's acceptable
          reserved_words = [/^alias$/i]
          reserved_words.each do |regex|
            if name =~ regex
              print_error "You cannot use #{name} as the name for an alias, sorry"
              return false
            end
          end

          if clear
            # clear all aliases if "*"
            if name == '*'
              @aliases.each_key do |a|
                deregister_alias(a)
              end
              print_status 'Cleared all aliases'
            elsif @aliases.keys.include?(name) # clear the named alias if it exists
              deregister_alias(name)
              print_status "Cleared alias #{name}"
            else
              print_error("#{name} is not a currently active alias")
            end
            return
          end
          # smash everything that's left together
          value = args.join(' ')
          value.strip!
          # value can NEVER be certain bad words like 'rm -rf /', add any other reserved words here
          # this is basic idiot protection, not meant to be impervious to subversive intentions
          reserved_words = [%r{^rm +(-rf|-r +-f|-f +-r) +/.*$}]
          reserved_words.each do |regex|
            if value =~ regex
              print_error "You cannot use #{value} as the value for an alias, sorry"
              return false
            end
          end

          is_valid_alias = valid_alias?(name, value)
          # print_good "Alias validity = #{is_valid_alias}"
          is_sys_cmd = Rex::FileUtils.find_full_path(name)
          is_already_alias = @aliases.keys.include?(name)
          if is_valid_alias && !is_sys_cmd && !is_already_alias
            register_alias(name, value)
          elsif force
            if !is_valid_alias
              print_status 'The alias failed validation, but force is set so we allow this.  This is often the case'
              print_status "when for instance 'exploit' is being overridden but msfconsole is not currently in the"
              print_status 'exploit context (an exploit is not loaded), or you are overriding a system command'
            end
            register_alias(name, value)
          else
            print_error("#{name} already exists as a system command, use -f to force override") if is_sys_cmd
            print_error("#{name} is already an alias, use -f to force override") if is_already_alias
            if !is_valid_alias && !force
              print_error("'#{name}' is not a permitted name or '#{value}' is not valid/permitted")
              print_error("It's possible the responding dispatcher isn't loaded yet, try changing to the proper context or using -f to force")
            end
          end
        end
      end

      def cmd_alias_help
        print_line 'Usage: alias [options] [name [value]]'
        print_line
        print(@@alias_opts.usage)
      end

      #
      # Tab completion for the alias command
      #
      def cmd_alias_tabs(_str, words)
        if words.length <= 1
          # puts "1 word or less"
          return @@alias_opts.option_keys + tab_complete_aliases_and_commands
        else
          # puts "more than 1 word"
          return tab_complete_aliases_and_commands
        end
      end

      private

      #
      # do everything needed to add an alias of +name+ having the value +value+
      #
      def register_alias(name, value)
        # TODO:  begin rescue?
        # TODO:  security concerns since we are using eval

        # define some class instance methods
        class_eval do
          # define a class instance method that will respond for the alias
          define_method "cmd_#{name}" do |*args|
            # just replace the alias w/the alias' value and run that
            driver.run_single("#{value} #{args.join(' ')}")
          end
          # define a class instance method that will tab complete the aliased command
          # we just proxy to the top-level tab complete function and let them handle it
          define_method "cmd_#{name}_tabs" do |str, words|
            # we need to repair the tab complete string/words and pass back
            # replace alias name with the root alias value
            value_words = value.split(/[\s\t\n]+/) # in case value is e.g. 'sessions -l'
            # valwords is now [sessions,-l]
            words[0] = value_words[0]
            # words[0] is now 'sessions' (was 'sue')
            value_words.shift # valwords is now ['-l']
            # insert any remaining parts of value and rebuild the line
            line = words.join(' ') + ' ' + value_words.join(' ') + ' ' + str

            [driver.tab_complete(line.strip), :override_completions]
          end
          # add a cmd_#{name}_help method
          define_method "cmd_#{name}_help" do |*_args|
            driver.run_single("help #{value}")
          end
        end
        # add the alias to the list
        @aliases[name] = value
      end

      #
      # do everything required to remove an alias of name +name+
      #
      def deregister_alias(name)
        class_eval do
          # remove the class methods we created when the alias was registered
          remove_method("cmd_#{name}")
          remove_method("cmd_#{name}_tabs")
          remove_method("cmd_#{name}_help")
        end
        # remove the alias from the list of active aliases
        @aliases.delete(name)
      end

      #
      # Validate a proposed alias with the +name+ and having the value +value+
      #
      def valid_alias?(name, value)
        # print_good "Assessing validay for #{name} and #{value}"
        # we validate two things, the name and the value

        ### name
        # we don't check if this alias name exists or if it's a console command already etc as -f can override
        # that so those need to be checked externally, we pretty much just check to see if the name is sane
        name.strip!
        bad_words = [/\*/] # add any additional "bad word" regexes here
        bad_words.each do |regex|
          # don't mess around, just return false in this case, prevents wasted processing
          return false if name =~ regex
        end

        ### value
        # value is considered valid if it's a ref to a valid console cmd, a system executable, or an existing
        # alias AND isn't a "bad word"
        # Here we check for "bad words" to avoid for the value...value would have to NOT match these regexes
        # this is just basic idiot protection
        value.strip!
        bad_words = [/^msfconsole$/]
        bad_words.each do |regex|
          # don't mess around, just return false if we match
          return false if value =~ regex
        end

        # we're only gonna validate the first part of the cmd, e.g. just ls from "ls -lh"
        value = value.split(' ').first
        return true if @aliases.keys.include?(value)

        [value, value + '.exe'].each do |cmd|
          return true if Rex::FileUtils.find_full_path(cmd)
        end

        # gather all the current commands the driver's dispatcher's have & check 'em
        driver.dispatcher_stack.each do |dispatcher|
          next unless dispatcher.respond_to?(:commands)
          next if dispatcher.commands.nil?
          next if dispatcher.commands.empty?

          if dispatcher.respond_to?("cmd_#{value.split(' ').first}")
            # print_status "Dispatcher (#{dispatcher.name}) responds to cmd_#{value.split(" ").first}"
            return true
          end
        end

        false
      end

      #
      # Provide tab completion list for aliases and commands
      #
      def tab_complete_aliases_and_commands
        items = []
        # gather all the current commands the driver's dispatcher's have
        driver.dispatcher_stack.each do |dispatcher|
          next unless dispatcher.respond_to?(:commands)
          next if (dispatcher.commands.nil? || dispatcher.commands.empty?)

          items.concat(dispatcher.commands.keys)
        end
        # add all the current aliases to the list
        items.concat(@aliases.keys)
        return items
      end

    end

    #
    # The constructor is called when an instance of the plugin is created.  The
    # framework instance that the plugin is being associated with is passed in
    # the framework parameter.  Plugins should call the parent constructor when
    # inheriting from Msf::Plugin to ensure that the framework attribute on
    # their instance gets set.
    #
    def initialize(framework, opts)
      super

      ## Register the commands above
      add_console_dispatcher(AliasCommandDispatcher)
    end

    #
    # The cleanup routine for plugins gives them a chance to undo any actions
    # they may have done to the framework.  For instance, if a console
    # dispatcher was added, then it should be removed in the cleanup routine.
    #
    def cleanup
      # If we had previously registered a console dispatcher with the console,
      # deregister it now.
      remove_console_dispatcher('Alias')

      # we don't need to remove class methods we added because they were added to
      # AliasCommandDispatcher class
    end

    #
    # This method returns a short, friendly name for the plugin.
    #
    def name
      'alias'
    end

    #
    # This method returns a brief description of the plugin.  It should be no
    # more than 60 characters, but there are no hard limits.
    #
    def desc
      'Adds the ability to alias console commands'
    end

  end
end
